﻿using System;
using System.Collections.Generic;
using System.IO;

namespace Aeon.Emulator.Mouse
{
    /// <summary>
    /// Provides the emulated mouse driver interface.
    /// </summary>
    internal sealed class MouseHandler : IInterruptHandler
    {
        private const byte CallbackInterrupt = 0x45;

        private VirtualMachine vm;
        private MouseState currentState;
        private MouseState callbackState;
        private CallbackMask callbackMask;
        private ushort callbackSegment;
        private ushort callbackOffset;
        private CallbackMask reason;
        private int showCount;
        private int motionCounterX;
        private int motionCounterY;
        private int callbackMotionCounterX;
        private int callbackMotionCounterY;
        private int minX;
        private int maxX = 639;
        private int minY;
        private int maxY = 199;
        private readonly ButtonPressTracker buttonTracker = new ButtonPressTracker();

        /// <summary>
        /// Initializes a new instance of the MouseHandler class.
        /// </summary>
        public MouseHandler()
        {
            this.UseDefaultVirtualWidth = true;
        }

        /// <summary>
        /// Gets the current position of the mouse cursor.
        /// </summary>
        public Aeon.Emulator.Video.Point Position
        {
            get { return new Aeon.Emulator.Video.Point(currentState.X, currentState.Y); }
        }
        /// <summary>
        /// Gets or sets a value indicating whether the default virtual width should be used.
        /// </summary>
        public bool UseDefaultVirtualWidth { get; set; }

        /// <summary>
        /// Notifies the mouse handler that the mouse was moved and relative coordinates are specified.
        /// </summary>
        /// <param name="deltaX">Relative horizontal movement.</param>
        /// <param name="deltaY">Relative vertical movement.</param>
        public void MouseMoveRelative(int deltaX, int deltaY)
        {
            motionCounterX += deltaX;
            motionCounterY += deltaY;

            callbackMotionCounterX += deltaX;
            callbackMotionCounterY += deltaY;

            currentState.X += deltaX;
            currentState.Y += deltaY;

            MouseMoved();
        }
        /// <summary>
        /// Notifies the mouse handler that the mouse was moved and absolute coordinates are specified.
        /// </summary>
        /// <param name="newX">New horizontal position.</param>
        /// <param name="newY">New vertical position.</param>
        public void MouseMoveAbsolute(uint newX, uint newY)
        {
            motionCounterX += (int)newX - currentState.X;
            motionCounterY += (int)newY - currentState.Y;

            callbackMotionCounterX += (int)newX - currentState.X;
            callbackMotionCounterY += (int)newY - currentState.Y;

            currentState.X = (int)newX;
            currentState.Y = (int)newY;

            MouseMoved();
        }
        /// <summary>
        /// Notifies the mouse handler the a mouse button was pressed.
        /// </summary>
        /// <param name="buttons">Button which was pressed.</param>
        public void MouseButtonDown(MouseButtons buttons)
        {
            buttonTracker.ButtonPress(buttons, currentState.X, currentState.Y);

            currentState.PressedButtons |= buttons;
            CallbackMask mask = GetCallbackReasonPress(buttons);

            if((mask & callbackMask) != 0)
            {
                reason = mask;
                vm.RaiseInterrupt(CallbackInterrupt);
            }
        }
        /// <summary>
        /// Notifies the mouse handler that a mouse button was released.
        /// </summary>
        /// <param name="buttons">Button which was released.</param>
        public void MouseButtonUp(MouseButtons buttons)
        {
            buttonTracker.ButtonRelease(buttons, currentState.X, currentState.Y);

            currentState.PressedButtons &= ~buttons;
            CallbackMask mask = GetCallbackReasonRelease(buttons);

            if((mask & callbackMask) != 0)
            {
                reason = mask;
                vm.RaiseInterrupt(CallbackInterrupt);
            }
        }

        /// <summary>
        /// Calls the user-specified callback function.
        /// </summary>
        private void RaiseCallback()
        {
            int scaledX = GetScaledX(currentState.X);
            int scaledY = GetScaledY(currentState.Y);

            vm.Processor.AX = (short)reason;
            vm.Processor.BX = (short)currentState.PressedButtons;
            vm.Processor.CX = (short)scaledX;
            vm.Processor.DX = (short)scaledY;
            vm.Processor.SI = (ushort)callbackMotionCounterX;
            vm.Processor.DI = (ushort)callbackMotionCounterY;
            callbackState = currentState;
            reason = CallbackMask.Disabled;

            Instructions.Call.FarAbsoluteCall(vm, (uint)((callbackSegment << 16) | callbackOffset));
        }
        /// <summary>
        /// Performs common handling of a move event.
        /// </summary>
        private void MouseMoved()
        {
            currentState.X = Math.Max(currentState.X, 0);
            currentState.X = Math.Min(currentState.X, vm.VideoMode.Width - 1);
            currentState.Y = Math.Max(currentState.Y, 0);
            currentState.Y = Math.Min(currentState.Y, vm.VideoMode.Height - 1);

            if((callbackMask & CallbackMask.Move) != 0)
            {
                reason = CallbackMask.Move;
                vm.RaiseInterrupt(CallbackInterrupt);
            }

            vm.OnMouseMove(new MouseMoveEventArgs(currentState.X, currentState.Y));
        }
        /// <summary>
        /// Returns the virtual horizontal cursor position.
        /// </summary>
        /// <param name="realX">Actual horizontal position.</param>
        /// <returns>Virtual horizontal cursor position.</returns>
        private int GetScaledX(int realX)
        {
            int screenWidth = vm.VideoMode.Width;
            int virtualWidth = maxX - minX + 1;
            double ratio = (double)virtualWidth / (double)screenWidth;
            return (int)(realX * ratio) + minX;
        }
        /// <summary>
        /// Returns the virtual vertical cursor position.
        /// </summary>
        /// <param name="realY">Actual vertical cursor position.</param>
        /// <returns>Virtual vertical cursor position.</returns>
        private int GetScaledY(int realY)
        {
            int screenHeight = vm.VideoMode.Height;
            int virtualHeight = maxY - minY + 1;
            double ratio = (double)virtualHeight / (double)screenHeight;
            return (int)(realY * ratio) + minY;
        }
        /// <summary>
        /// Returns the actual horizontal cursor position.
        /// </summary>
        /// <param name="scaledX">Scaled value to convert.</param>
        /// <returns>Actual horizontal cursor position.</returns>
        private int GetRealX(int scaledX)
        {
            int screenWidth = vm.VideoMode.Width;
            int virtualWidth = maxX - minX + 1;
            double ratio = (double)screenWidth / (double)virtualWidth;
            return (int)((scaledX - minX) * ratio);
        }
        /// <summary>
        /// Returns the actual vertical cursor position.
        /// </summary>
        /// <param name="scaledX">Scaled value to convert.</param>
        /// <returns>Actual vertical cursor position.</returns>
        private int GetRealY(int scaledY)
        {
            int screenHeight = vm.VideoMode.Height;
            int virtualHeight = maxY - minY + 1;
            double ratio = (double)screenHeight / (double)virtualHeight;
            return (int)((scaledY - minY) * ratio);
        }

        /// <summary>
        /// Converts a MouseButtons value to a corresponding CallbackMask for a button press.
        /// </summary>
        /// <param name="button">Button to convert.</param>
        /// <returns>Converted CallbackMask value.</returns>
        private static CallbackMask GetCallbackReasonPress(MouseButtons button)
        {
            switch(button)
            {
            case MouseButtons.Left:
                return CallbackMask.LeftButtonDown;

            case MouseButtons.Right:
                return CallbackMask.RightButtonDown;

            case MouseButtons.Middle:
                return CallbackMask.MiddleButtonDown;

            default:
                return CallbackMask.Disabled;
            }
        }
        /// <summary>
        /// Converts a MouseButtons value to a corresponding CallbackMask for a button release.
        /// </summary>
        /// <param name="button">Button to convert.</param>
        /// <returns>Converted CallbackMask value.</returns>
        private static CallbackMask GetCallbackReasonRelease(MouseButtons button)
        {
            switch(button)
            {
            case MouseButtons.Left:
                return CallbackMask.LeftButtonUp;

            case MouseButtons.Right:
                return CallbackMask.RightButtonUp;

            case MouseButtons.Middle:
                return CallbackMask.MiddleButtonUp;

            default:
                return CallbackMask.Disabled;
            }
        }

        IEnumerable<InterruptHandlerInfo> IInterruptHandler.HandledInterrupts
        {
            get { return new InterruptHandlerInfo[] { 0x33, new InterruptHandlerInfo(CallbackInterrupt, Registers.AX | Registers.BX | Registers.CX | Registers.DX | Registers.BP | Registers.SI | Registers.DI | Registers.DS | Registers.ES, false, true) }; }
        }
        void IInterruptHandler.HandleInterrupt(int interrupt)
        {
            if(interrupt == CallbackInterrupt)
            {
                this.RaiseCallback();
                return;
            }

            switch((ushort)vm.Processor.AX)
            {
            case Functions.Reset:
                vm.Processor.AX = -1; // Indicates mouse installed.
                vm.Processor.BX = 3; // Three-button mouse.
                showCount = 0;  // Mouse cursor invisible.
                vm.IsMouseVisible = false;
                minX = 0;
                minY = 0;
                maxY = this.UseDefaultVirtualWidth ? 479 : 199;
                maxX = this.UseDefaultVirtualWidth ? 639 : 319;
                callbackMotionCounterX = 0;
                callbackMotionCounterY = 0;
                break;

            case Functions.SoftwareReset:
                vm.Processor.AX = -1; // Indicates mouse installed.
                vm.Processor.BX = 3; // Three-button mouse.
                break;

            case Functions.EnableMouseDriver:
                // Always return success.
                vm.Processor.AX = 0x20;
                break;

            case Functions.ShowCursor:
                showCount++;
                if(showCount == 1)
                    vm.IsMouseVisible = true;
                break;

            case Functions.HideCursor:
                showCount--;
                if(showCount == 0)
                    vm.IsMouseVisible = false;
                break;

            case Functions.GetPositionAndStatus:
                vm.Processor.CX = (short)GetScaledX(currentState.X);
                vm.Processor.DX = (short)GetScaledY(currentState.Y);
                vm.Processor.BX = (short)currentState.PressedButtons;
                break;

            case Functions.SetCursorPosition:
                // Bad things can happen if we don't filter out the useless messages.
                if(currentState.X != GetRealX(vm.Processor.CX) || currentState.Y != GetRealY(vm.Processor.DX))
                {
                    currentState.X = GetRealX(vm.Processor.CX);
                    currentState.Y = GetRealY(vm.Processor.DX);
                    var args = new MouseMoveEventArgs(currentState.X, currentState.Y);
                    vm.OnMouseMove(args);
                    vm.OnMouseMoveByEmulator(args);
                }
                break;

            case Functions.SetHorizontalRange:
                minX = vm.Processor.CX;
                maxX = vm.Processor.DX;
                break;

            case Functions.SetVerticalRange:
                minY = vm.Processor.CX;
                maxY = vm.Processor.DX;
                break;

            case Functions.GetMotionCounters:
                vm.Processor.CX = (short)motionCounterX;
                vm.Processor.DX = (short)motionCounterY;
                motionCounterX = 0;
                motionCounterY = 0;
                break;

            case Functions.SetCallbackParameters:
                callbackMask = (CallbackMask)vm.Processor.CX;
                callbackSegment = vm.Processor.ES;
                callbackOffset = (ushort)vm.Processor.DX;
                break;

            case Functions.GetButtonPressData:
                var pressInfo = buttonTracker.GetButtonPressInfo(vm.Processor.BX);
                vm.Processor.BX = (short)(pressInfo.Count & 0x7FFFu);
                vm.Processor.CX = (short)GetScaledX(pressInfo.X);
                vm.Processor.DX = (short)GetScaledY(pressInfo.Y);
                break;

            case Functions.GetButtonReleaseData:
                var releaseInfo = buttonTracker.GetButtonReleaseInfo(vm.Processor.BX);
                vm.Processor.BX = (short)(releaseInfo.Count & 0x7FFFu);
                vm.Processor.CX = (short)GetScaledX(releaseInfo.X);
                vm.Processor.DX = (short)GetScaledY(releaseInfo.Y);
                break;

            case Functions.ExchangeCallbacks:
                var newMask = (CallbackMask)vm.Processor.CX;
                var newSegment = vm.Processor.ES;
                var newOffset = (ushort)vm.Processor.DX;

                vm.Processor.CX = (short)callbackMask;
                vm.WriteSegmentRegister(SegmentIndex.ES, callbackSegment);
                vm.Processor.DX = (short)callbackOffset;

                callbackMask = newMask;
                callbackSegment = newSegment;
                callbackOffset = newOffset;
                break;

            case Functions.SetMickeyPixelRatio:
                // Ignore this for now.
                break;

            case Functions.GetDriverStateStorageSize:
                vm.Processor.AX = 0x200;
                break;

            case Functions.SaveDriverState:
            case Functions.RestoreDriverState:
                // Ignore this for now.
                break;

            default:
                System.Diagnostics.Debug.WriteLine(string.Format("Mouse function {0:X2}h not implemented.", vm.Processor.AX));
                break;
            }
        }

        void IVirtualDevice.Pause()
        {
        }
        void IVirtualDevice.Resume()
        {
        }
        /// <summary>
        /// Invoked when the virtual device has been added to a VirtualMachine.
        /// </summary>
        /// <param name="vm">VirtualMachine which owns the device.</param>
        void IVirtualDevice.DeviceRegistered(VirtualMachine vm)
        {
            this.vm = vm;
        }

        void IDisposable.Dispose()
        {
        }
        /// <summary>
        /// Invoked when the device should save its current state to a <see cref="System.IO.Stream"/>.
        /// </summary>
        /// <param name="destination"><see cref="System.IO.Stream"/> to which the state is saved.</param>
        void IVirtualDevice.SaveCurrentState(Stream destination)
        {
            var writer = new BinaryWriter(destination);
            writer.Write(this.currentState.X);
            writer.Write(this.currentState.Y);
            writer.Write((int)this.currentState.PressedButtons);

            writer.Write(this.callbackState.X);
            writer.Write(this.callbackState.Y);
            writer.Write((int)this.callbackState.PressedButtons);

            writer.Write((int)this.callbackMask);
            writer.Write(this.callbackSegment);
            writer.Write(this.callbackOffset);
            writer.Write((int)this.reason);

            writer.Write(this.showCount);
            
            writer.Write(this.motionCounterX);
            writer.Write(this.motionCounterY);
            writer.Write(this.callbackMotionCounterX);
            writer.Write(this.callbackMotionCounterY);
            
            writer.Write(this.minX);
            writer.Write(this.maxX);

            writer.Write(this.minY);
            writer.Write(this.maxY);

            this.buttonTracker.SaveCurrentState(writer);
        }
        /// <summary>
        /// Invoked when the device should restore its state from a <see cref="System.IO.Stream"/>.
        /// </summary>
        /// <param name="source"><see cref="System.IO.Stream"/> from which the state is restored.</param>
        void IVirtualDevice.RestoreState(Stream source)
        {
            var reader = new BinaryReader(source);
            this.currentState.X = reader.ReadInt32();
            this.currentState.Y = reader.ReadInt32();
            this.currentState.PressedButtons = (MouseButtons)reader.ReadInt32();

            this.callbackState.X = reader.ReadInt32();
            this.callbackState.Y = reader.ReadInt32();
            this.callbackState.PressedButtons = (MouseButtons)reader.ReadInt32();

            this.callbackMask = (CallbackMask)reader.ReadInt32();
            this.callbackSegment = reader.ReadUInt16();
            this.callbackOffset = reader.ReadUInt16();
            this.reason = (CallbackMask)reader.ReadInt32();

            this.showCount = reader.ReadInt32();

            this.motionCounterX = reader.ReadInt32();
            this.motionCounterY = reader.ReadInt32();
            this.callbackMotionCounterX = reader.ReadInt32();
            this.callbackMotionCounterY = reader.ReadInt32();

            this.minX = reader.ReadInt32();
            this.maxX = reader.ReadInt32();

            this.minY = reader.ReadInt32();
            this.maxY = reader.ReadInt32();

            this.buttonTracker.RestoreState(reader);
        }
    }
}
